package org.eclipsecon.gwt.chattr.server;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import org.eclipsecon.gwt.chattr.client.ChatEvent;
import org.eclipsecon.gwt.chattr.client.ChatEventVisitor;
import org.eclipsecon.gwt.chattr.client.ChatService;
import org.eclipsecon.gwt.chattr.client.ChatServiceException;
import org.eclipsecon.gwt.chattr.client.NewMessageEvent;
import org.eclipsecon.gwt.chattr.client.User;
import org.eclipsecon.gwt.chattr.client.UserAddEvent;
import org.eclipsecon.gwt.chattr.client.UserRemoveEvent;
import org.eclipsecon.gwt.chattr.client.UserUpdateEvent;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Servlet which implements the {@link ChatService} service.
 */
public class ChatServiceImpl extends RemoteServiceServlet implements
    ChatService {

  /**
   * 
   */
  private final class ChatEventHandler extends ChatEventVisitor {
    public void visit(NewMessageEvent event) {
      List recipients = event.getRecipients();

      if (recipients != null) {
        propagateEvent(recipients.iterator(), event);
      }
    }

    public void visit(UserAddEvent event) {
      // The server sends this message but it should never receive it
    }

    public void visit(UserRemoveEvent event) {
      // The server should send this message but it should never receive it
    }

    public void visit(UserUpdateEvent event) {
      Set keys = userToUserInfoMap.keySet();
      keys.remove(event.getUser());

      UserInfo userInfo = findUserInfoForUser(event.getUser());
      assert (userInfo != null);
      userInfo.user.setStatusMessage(event.getUser().getStatusMessage());

      propagateEvent(keys.iterator(), event);
    }
  }

  private final class SuperUserInfo extends UserInfo {
    public SuperUserInfo(User user) {
      super(user);
    }

    public void addEvent(ChatEvent event) {
      if (event instanceof NewMessageEvent) {
        NewMessageEvent newMsgEvt = (NewMessageEvent) event;

        UserInfo userInfo = findUserInfoForUser(newMsgEvt.getSender());
        if (userInfo != null) {
          userInfo.addEvent(new NewMessageEvent(getUser(), new ArrayList(
              Collections.singletonList(newMsgEvt.getSender())),
              getUser().getStatusMessage()));
        }
      }
    }
  }

  /**
   * 
   */
  private class UserInfo {
    private static final long DEFAULT_INACTIVITY_TIMEOUT_MILLIS = 10000;
    private List eventQueue = new ArrayList();
    private TimerTask inactivityTimerTask;

    private final User user;

    public UserInfo(User user) {
      this.user = user;
    }

    public void addEvent(ChatEvent event) {
      eventQueue.add(event);
    }

    public void clearEventQueue() {
      eventQueue = new ArrayList();
    }

    public List getEventQueue() {
      return eventQueue;
    }

    public User getUser() {
      return user;
    }

    public void resetInactivityTimer() {
      if (inactivityTimerTask != null) {
        inactivityTimerTask.cancel();
      }

      inactivityTimerTask = new TimerTask() {
        public void run() {
          userToUserInfoMap.remove(user);

          fireUserRemovedEvent(user);
        }
      };

      try {

        inactivityTimer.schedule(inactivityTimerTask,
            DEFAULT_INACTIVITY_TIMEOUT_MILLIS);
      } catch (IllegalStateException ex) {
        ex.printStackTrace();
      }
    }
  }

  private ChatEventHandler eventHandler = new ChatEventHandler();

  /**
   * 
   */
  private Timer inactivityTimer = new Timer();

  /**
   * Map of user names to event queues
   */
  private final Map userToUserInfoMap = new HashMap();

  public ChatServiceImpl() {
    // Default to George P. Burdell
    User user = new User("George P. Burdell", "Go Jackets!");
    UserInfo userInfo = new SuperUserInfo(user);
    userToUserInfoMap.put(user, userInfo);
  }

  /**
   * 
   * <ul>
   * <li>Get the user - if non-existant create the user and add an event to all
   * of the other user's event queues</li>
   * <li>Kick the inactivity timer</li>
   * <li>Process all of the events sent by this user user</li>
   * <li>Get all of the events queued for this user</li>
   * <li>clear the user's event queue</li>
   * </ul>
   * 
   * @throws ChatServiceException
   */
  public List exchangeEvents(User user, List clientEvents)
      throws ChatServiceException {

    if (user == null) {
      // RuntimeExceptions are not serialized by default
      throw new ChatServiceException("User cannot be null");
    }

    UserInfo userInfo = findUserInfoForUser(user);
    if (userInfo == null) {
      userInfo = createUser(user);
    }

    userInfo.resetInactivityTimer();

    if (clientEvents != null) {
      processClientEvents(clientEvents);
    }

    List events = userInfo.getEventQueue();

    userInfo.clearEventQueue();

    return events;
  }

  public List getCurrentUsers(User user) {
    UserInfo userInfo = findUserInfoForUser(user);
    if (userInfo == null) {
      userInfo = createUser(user);
    }

    List currentUsers = new ArrayList(userToUserInfoMap.keySet());
    currentUsers.remove(user);

    return currentUsers;
  }

  private UserInfo createUser(User user) {
    UserInfo userInfo = new UserInfo(user);

    fireNewUserEvent(user);

    notifyNewUserOfExistingUsers(userInfo);

    userToUserInfoMap.put(user, userInfo);
    return userInfo;
  }

  /**
   * Returns the {@link UserInfo} for a given {@link User}.
   * 
   * @param user the user whose UserInfo we want to lookup
   * @return UserInfo for the given user or null, if no such user exists
   */
  private UserInfo findUserInfoForUser(User user) {
    return (UserInfo) userToUserInfoMap.get(user);
  }

  /**
   * 
   * @param user
   */
  private void fireNewUserEvent(User user) {
    UserAddEvent userAddEvent = new UserAddEvent(user);

    propagateEvent(userToUserInfoMap.keySet().iterator(), userAddEvent);
  }

  /**
   * 
   * @param user
   */
  private void fireUserRemovedEvent(User user) {
    UserRemoveEvent userRemovedEvent = new UserRemoveEvent(user);

    propagateEvent(userToUserInfoMap.keySet().iterator(), userRemovedEvent);
  }

  private void notifyNewUserOfExistingUsers(UserInfo newUserInfo) {
    Iterator iter = userToUserInfoMap.keySet().iterator();

    while (iter.hasNext()) {
      User user = (User) iter.next();

      newUserInfo.addEvent(new UserAddEvent(user));
    }
  }

  /**
   * Processes the list of events that a client sent.
   * 
   * @param clientEvents list of events that a client sent
   */
  private void processClientEvents(List clientEvents) {
    assert (clientEvents != null);

    Iterator iter = clientEvents.iterator();
    while (iter.hasNext()) {
      ChatEvent chatEvent = (ChatEvent) iter.next();

      chatEvent.accept(eventHandler);
    }
  }

  /**
   * Propagates an event to a set of users.
   * 
   * @param iterRecipients iterator over set of recipients
   * @param event the event that we want to propagate
   */
  private void propagateEvent(Iterator iterRecipients, ChatEvent event) {
    while (iterRecipients.hasNext()) {
      User recipient = (User) iterRecipients.next();

      UserInfo userInfo = findUserInfoForUser(recipient);
      if (userInfo != null) {
        userInfo.addEvent(event);
      }
    }
  }
}
